package com.game.service;

import com.game.SysConfig;
import com.game.dao.LoginLogDAO;
import com.game.dao.PlayerDAO;
import com.game.domain.player.Player;
import com.game.domain.player.PlayerData;
import com.game.domain.player.StepType;
import com.game.sdk.http.HttpClient;
import com.game.sdk.net.Result;
import com.game.sdk.proto.GainStepBallResp;
import com.game.sdk.proto.GetRoleDetailResp;
import com.game.sdk.proto.GetRoleResp;
import com.game.sdk.proto.OpenIDResp;
import com.game.sdk.proto.vo.GiveStepVO;
import com.game.sdk.proto.vo.StepBallVO;
import com.game.sdk.utils.DecoderHandler;
import com.game.sdk.utils.ErrorCode;
import com.game.util.JsonUtils;
import com.game.util.TimeUtil;
import com.game.util.TimerService;
import com.google.common.cache.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class PlayerService extends AbstractService {
    private static Logger logger = Logger.getLogger(PlayerService.class);
    private Map<String, String> sessionMap = Maps.newHashMap();         //session保存，一段时间清空

    @Autowired
    private TimerService timerService;
    @Autowired
    private PlayerDAO playerDAO;
    @Autowired
    private LoginLogDAO loginLogDAO;
    @Autowired
    private BroadcastService broadcastService;

    static final int MAX_GIVE_STEP = 3000;
    static final float STEP_TO_CALORIE_PERSECOND = 1.39f;   //每秒步数转换成卡路里
    static final float CALORIE_PERSTEP = 0.025f;            //每步消耗多少卡路里

    //TODO 可以优化为停服时统一保存
    private final LoadingCache<String, Player> players = CacheBuilder.newBuilder()
            .expireAfterAccess(600, TimeUnit.SECONDS)
            .maximumSize(5000)
            .removalListener(new RemovalListener<String, Player>() {
                @Override
                public void onRemoval(RemovalNotification<String, Player> notification) {
                    saveOrUpdate(notification.getValue());
                }
            })
            .build(new CacheLoader<String, Player>() {
                @Override
                public Player load(String openId) throws Exception {
                    logger.info("Cache loaded for " + openId);
                    Player player = playerDAO.queryPlayer(openId);
                    PlayerData playerData = JsonUtils.string2Object(player.getPlayerData(), PlayerData.class);
                    player.setPlayerDataObject(playerData);
                    return player;
                }
            });


    @Override
    public void onStart() {
        //5分钟定时
        timerService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    schedule();
                } catch (Exception e) {
                    logger.error("match schedule error", e);
                }
            }
        }, 0, 5, TimeUnit.MINUTES);
    }

    /**
     * 保存或更新
     *
     * @param player
     */
    public void saveOrUpdate(Player player) {
        player.setPlayerData(JsonUtils.object2String(player.getPlayerDataObject()));
        playerDAO.saveOrUpdate(player);
    }

    /**
     * 获取玩家信息
     *
     * @param openId
     * @return
     */
    public Player getPlayer(String openId) {
        try {
            return players.get(openId);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 创建角色
     *
     * @param openId
     * @param nickName
     */
    public Result createPlayer(String openId, String nickName, String iconUrl) {
        Player player = new Player();
        player.setNickName(nickName);
        player.setOpenId(openId);
        player.setLevel(1);
        player.setCalorie(0);
        player.getPlayerDataObject().setIconUrl(iconUrl);
        player.setCreateTime(System.currentTimeMillis());
        player.setLoginTime(System.currentTimeMillis());
        saveOrUpdate(player);
        players.put(openId, player);

        return Result.valueOf(ErrorCode.OK, "ok");
    }

    /**
     * 获取OPENID
     *
     * @param openId
     * @param code
     * @return
     * @throws Exception
     */
    public Result getOpenID(String openId, String code) throws Exception {
        String errorCode = ErrorCode.OK;
        OpenIDResp resp = new OpenIDResp();
        String session;
        if (!"".equals(code))//测试模式
        {
            String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + SysConfig.wxAppid + "&secret=" + SysConfig.wxAppSecret + "&grant_type=authorization_code&js_code=" + code;
            String json = HttpClient.sendGetRequest(url);
            Map<String, Object> result = JsonUtils.string2Map(json);
            openId = (String) result.get("openid");
            session = (String) result.get("session_key");
        } else {
            session = "test" + openId;
        }
        try {
            players.get(openId);
            resp.setHasRole(true);
        } catch (Exception e) {
            resp.setHasRole(false);
        }

        logger.warn("player openId: " + openId + ", session: " + session);

        sessionMap.put(openId, session);
        resp.setOpenId(openId);

        return Result.valueOf(errorCode, resp);
    }

    /**
     * 创建角色
     *
     * @param openId
     */
    public Result getRole(String openId) {
        String code = ErrorCode.OK;
        GetRoleResp resp = new GetRoleResp();
        Player player = players.getUnchecked(openId);
        if (player == null) {
            code = ErrorCode.ROLE_NOT_EXIST;
        } else {
            resp.setOpenId(player.getOpenId());
            resp.setLevel(player.getLevel());
            resp.setCalorie(player.getCalorie());
            long lastLoginTime = player.getLoginTime();
            long today = TimeUtil.getTodayBeginTime();
            resp.setFirstLogin(!TimeUtil.isSameDate(today, lastLoginTime));

            Instant instant1 = Instant.ofEpochMilli(player.getLoginTime());
            LocalDateTime localDateTime1 = LocalDateTime.ofInstant(instant1, ZoneId.systemDefault());
            Instant instant2 = Instant.ofEpochMilli(System.currentTimeMillis());
            LocalDateTime localDateTime2 = LocalDateTime.ofInstant(instant2, ZoneId.systemDefault());
            if (!localDateTime1.toLocalDate().isEqual(localDateTime2.toLocalDate())) { //每日重置
                loginLogDAO.insert(player.getOpenId(), new Date());

                player.getPlayerDataObject().setTodayTransStep(0);
            }
            player.setLoginTime(System.currentTimeMillis());
            //登陆初始化道具ID起始位置
            player.getPlayerDataObject().getBag().loadIdgen();
            saveOrUpdate(player);
        }

        return Result.valueOf(code, resp);
    }


    private void schedule() {
        logger.warn("rank .....");
    }

    /**
     * 更新角色信息
     *
     * @param openId
     */
    public Result updateRole(String openId, String nick, String iconUrl) {
        Player player = players.getUnchecked(openId);
        if (nick != null && nick.length() > 0) {
            player.setNickName(nick);
        }
        if (iconUrl != null && iconUrl.length() > 0) {
            player.getPlayerDataObject().setIconUrl(iconUrl);
        }
        saveOrUpdate(player);

        return Result.valueOf(ErrorCode.OK, "ok");
    }

    /**
     * 获取用户详细数据
     *
     * @param openId
     */
    public Result getRoleDetailInfo(String openId) {
        Player player = players.getUnchecked(openId);
        if (player == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        GetRoleDetailResp resp = new GetRoleDetailResp();
        this.updateStepAndCalorie(openId);

        resp.setOpenId(player.getOpenId());
        resp.setNickName(player.getNickName());
        resp.setIconUrl(player.getPlayerDataObject().getIconUrl());
        resp.setLevel(player.getLevel());
        resp.setCalorie(player.getCalorie());
        resp.setStep(player.getPlayerDataObject().getStep());

        return Result.valueOf(ErrorCode.OK, resp);
    }

    /**
     * 向微信端获取步数
     *
     * @param openId
     * @param encryptedData
     * @param iv
     * @return
     */
    public Result getRunData(String openId, String encryptedData, String iv) {
        String sessionKey = sessionMap.getOrDefault(openId, "");
        if (sessionKey.equals("")) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        Player player = getPlayer(openId);
        if (player == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        int step = 0;
        //测试
        if (sessionKey.startsWith("test")) {
            step = player.getPlayerDataObject().getTodayTransStep() + 10000;
        } else {
            String json = DecoderHandler.decrypt(encryptedData, iv, sessionKey);
            if (json == null) {
                return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
            }

            Map<String, Object> result = JsonUtils.string2Map(json);
            Object[] steps = ((ArrayList) result.get("stepInfoList")).toArray();
            Map<String, Integer> lastStep = (Map<String, Integer>) steps[steps.length - 1];
            int timeStamp = lastStep.get("timestamp");
            boolean isSameDay = TimeUtil.isSameDate(Calendar.getInstance().getTimeInMillis() / 1000, timeStamp);

            step = isSameDay ? lastStep.get("step") : 0;
        }
        int todayTransStep = player.getPlayerDataObject().getTodayTransStep();
        int add = step - todayTransStep;

        player.getPlayerDataObject().setTodayTransStep(step);
        this.generateStepBall(openId, add, StepType.Normal, null);

        return Result.valueOf(ErrorCode.OK, add);
    }


    public Result giveStep(String openId, String giveOpenId) {
        Player player = getPlayer(openId);
        if (player == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        Player givePlayer = getPlayer(giveOpenId);
        if (givePlayer == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        //获取走路步数球总步数
        List<StepBallVO> list = player.getPlayerDataObject().getStepBallMap().get(StepType.Normal);
        if (list == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        int giveStep = 0;
        for (int index = 0; index < list.size(); index++) {
            StepBallVO vo = list.get(index);
            int step = Math.min(MAX_GIVE_STEP - giveStep, vo.getStep());

            if (step == vo.getStep()) {
                list.remove(index);
                index--;
            }

            giveStep += step;

            if (giveStep == MAX_GIVE_STEP) {
                break;
            }
        }
        saveOrUpdate(player);

        if (giveStep > 0) {
            GiveStepVO vo = new GiveStepVO();
            vo.setOpenId(player.getOpenId());
            vo.setNickName(player.getNickName());
            vo.setIconUrl(player.getPlayerDataObject().getIconUrl());
            vo.setStep(giveStep);
            this.generateStepBall(giveOpenId, giveStep, StepType.Give, vo);
        }

        return Result.valueOf(ErrorCode.OK, giveStep);
    }

    /**
     * 查询步数球列表
     *
     * @param openId
     * @return
     */
    public Result getStepBalls(String openId) {
        Player player = getPlayer(openId);
        if (player == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        List<StepBallVO> ret = Lists.newArrayList();
        Map<StepType, List<StepBallVO>> map = player.getPlayerDataObject().getStepBallMap();

        if (map == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        for (StepType type : map.keySet()) {
            ret.addAll(map.get(type));
        }

        return Result.valueOf(ErrorCode.OK, ret);
    }

    /**
     * 收获步数球
     *
     * @param openId
     * @param id
     * @param type
     * @return
     */
    public Result gainStepBall(String openId, String id, int type) {
        Player player = getPlayer(openId);
        if (player == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        List<StepBallVO> list = player.getPlayerDataObject().getStepBallMap().get(StepType.values()[type]);
        StepBallVO stepVo = null;

        for (StepBallVO vo : list) {
            if (vo.getId().equals(id)) {
                stepVo = vo;
                break;
            }
        }

        if (stepVo == null) {
            return Result.valueOf(ErrorCode.PARAM_ERROR, "error");
        }

        //收获步数
        GainStepBallResp resp = new GainStepBallResp();
        int gainStep = stepVo.getStep();
        list.remove(stepVo);

        if (gainStep > 0) {
            this.updateStepAndCalorie(openId);
            player.getPlayerDataObject().setStep(player.getPlayerDataObject().getStep() + gainStep);
            saveOrUpdate(player);
        }

        return Result.valueOf(ErrorCode.OK, resp);
    }

    private void updateStepAndCalorie(String openId) {
        Player player = getPlayer(openId);
        if (player == null) {
            return;
        }

        float step = player.getPlayerDataObject().getStep();
        long now = TimeUtil.getTimeNow();

        if (step <= 0) {
            player.getPlayerDataObject().setLastCalculateTime(now);
            return;
        }

        float toCalorieStep = Math.min(step, (now - player.getPlayerDataObject().getLastCalculateTime()) / 1000 * STEP_TO_CALORIE_PERSECOND);
        if (toCalorieStep > 0) {
            //计算卡路里
            player.setCalorie(player.getCalorie() + toCalorieStep * CALORIE_PERSTEP);
        }

        player.getPlayerDataObject().setStep(step - toCalorieStep);
        player.getPlayerDataObject().setLastCalculateTime(now);
    }

    /**
     * 生成步数球
     *
     * @param openId
     * @param step
     * @param type
     * @param param
     */
    private int generateStepBall(String openId, int step, StepType type, GiveStepVO param) {
        Player player = players.getUnchecked(openId);

        if (step <= 0) {
            return 0;
        }

        List<StepBallVO> list = player.getPlayerDataObject().getStepBallMap().get(type);
        boolean isFind = false;

        if (type == StepType.Give) {
            for (StepBallVO vo : list) {
                if (vo.getParam().getOpenId().equals(param.getOpenId())) {
                    vo.setStep(vo.getStep() + step);
                    isFind = true;
                    break;
                }
            }
        }

        if (!isFind) {
            StepBallVO vo = new StepBallVO();
            vo.setId(UUID.randomUUID().toString());
            vo.setType(type.ordinal());
            vo.setStep(step);
            vo.setParam(param);

            list.add(vo);
        }

        return step;
    }
}
